package javax.per.toolkit.security;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.UUID;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.per.toolkit.file.FileUtil;
import javax.per.toolkit.tools.Log;
import javax.per.toolkit.tools.Log.LogLevel;
import javax.per.toolkit.tools.msg.Msg;

/**
 * 
 * @author yl34569
 *
 */

public class FileEncryptor {
	
	/**
	 * Files to be encrypted or decrypted.
	 */
	private List<File> list=new ArrayList<File>();
	
	/**
	 * The type to be used for encrypting.
	 */
	private DataEncryptor.encryptType type=DataEncryptor.encryptType.DES;
	
	/**
	 * Setting of the output directory.
	 */
	private String outputDir=null;
	
	/**
	 * The secret key.
	 */
	private String key=null;
	
	/**
	 * Default PBE salt.
	 */
	private byte[] PBESalt={0,0,0,0,0,0,0,0};
	
	/**
	 * Determine whether to use a default PBE salt or not.
	 */
	private boolean isRandomSalt=false;
	
	/**
	 * Execute the initialization check.
	 */
	protected final void initCheck(){
		if(outputDir==null)
			throw new Error(Msg.get("ODNSE"));
		File f=new File(outputDir);
		f.mkdirs();
	}
	
	/**
	 * Set the salt of the PBE.
	 * @param filePath a file contains the salt
	 * @throws IOException
	 */
	public FileEncryptor setPBESalt(String filePath) throws IOException{
		File f=new File(filePath);
		InputStream is=new FileInputStream(f);
		is.read(PBESalt);
		is.close();
		return this;
	}
	
	public FileEncryptor setRandomPEBSalt(){
		Random ran=new Random();
		for(int i=0;i<this.PBESalt.length;i++){
			this.PBESalt[i]=(byte) ran.nextInt();
		}
		return this;
	}
	
	/**
	 * Set the secret key.
	 * @param key the key.
	 * @return a instance of this class.
	 */
	public FileEncryptor setKey(String key) {
		this.key = key;
		return this;
	}
	
	public FileEncryptor setKey(File file) throws IOException {
		BufferedReader br=FileUtil.getReader(file.getAbsolutePath());
		this.key="";
		int tmp;
		while((tmp=br.read())!=-1){
			this.key+=(char)tmp;
		}
		Log.getLogger().debug(this.key);
		return this;
	}
	
	/**
	 * Set output path.
	 * @param outputDir the directory.
	 * @return a instance of this class.
	 */
	public FileEncryptor setOutputDir(String outputDir) {
		this.outputDir = outputDir;
		return this;
	}
	
	/**
	 * Setting if to show debug information.
	 * @param showInfo show or not.
	 * @return  a instance of this class.
	 */
	public FileEncryptor setDebug(boolean showInfo) {
		if(!showInfo)
			Log.setLevel(LogLevel.OFF);
		else
			Log.setLevel(LogLevel.DEBUG);
		return this;
	}

	/**
	 * Add file to be encrypted or decrypted.
	 * @param filePath path of a file.
	 * @return a instance of this class.
	 */
	public FileEncryptor putFile(String filePath){
		File f=new File(filePath);
		this.putFile(f);
		return this;
	}
	
	/**
	 * Add file to be encrypted or decrypted.
	 * @param file a file
	 * @return a instance of this class.
	 */
	public FileEncryptor putFile(File file){
		if(file==null||!file.exists())
			throw new Error(Msg.get("FNF"));
		list.add(file);
		return this;
	}
	
	/**
	 * Set the encrypt type.
	 * @param type the type.
	 * @return  a instance of this class.
	 */
	public FileEncryptor setEncryptType(DataEncryptor.encryptType type){
		this.type=type;
		return this;
	}
	
	/**
	 * Encrypt the files.
	 * @throws IOException
	 * @throws InvalidKeyException
	 * @throws NoSuchAlgorithmException
	 * @throws InvalidKeySpecException
	 * @throws NoSuchPaddingException
	 * @throws IllegalBlockSizeException
	 * @throws BadPaddingException
	 */
	public void encryptFiles() throws IOException, InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException{
		initCheck();
		key=DataEncryptor.generateKeyWith(type);
		Log.getLogger().debug("Key saved in file \'"+createKeyFile()+"\', Ps keep it.");
		Iterator<File> it=list.iterator();
		while(it.hasNext()){
			File tmp=it.next();
			showInfo(tmp.getAbsolutePath(),"Encrypt");
			File targetFile=new File(this.outputDir+File.separator+tmp.getName());
			if(targetFile.equals(tmp))
				throw new Error("the source file is equal to the target file");
			if(targetFile.exists()){
				Log.getLogger().debug("Warning: target file already exist, deleting");
				targetFile.delete();
			}
			targetFile.getParentFile().mkdirs();
			targetFile.createNewFile();
			
			InputStream is=new FileInputStream(tmp);
			OutputStream os=new FileOutputStream(targetFile);
			DataEncryptor.encryptWith(is,os, key, type);
			is.close();
			os.close();
		}
	}
	
	/**
	 * Decrypt the files.
	 * @throws IOException
	 * @throws InvalidKeyException
	 * @throws NoSuchAlgorithmException
	 * @throws InvalidKeySpecException
	 * @throws NoSuchPaddingException
	 * @throws IllegalBlockSizeException
	 * @throws BadPaddingException
	 */
	public void decryptFiles() throws IOException, InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException{
		initCheck();
		if(key==null&&!autoSetKey())
			throw new Error("Key is not set");
		Iterator<File> it=list.iterator();
		while(it.hasNext()){
			File tmp=it.next();
			showInfo(tmp.getAbsolutePath(),"Decrypt");
			File targetFile=new File(this.outputDir+File.separator+tmp.getName());
			if(targetFile.equals(tmp))
				throw new Error("the source file is equal to the target file");
			if(targetFile.exists()){
				Log.getLogger().debug("Warning: target file already exist, deleting");
				targetFile.delete();
			}
			targetFile.getParentFile().mkdirs();
			targetFile.createNewFile();
			InputStream is=new FileInputStream(tmp);
			OutputStream os=new FileOutputStream(targetFile);
			DataEncryptor.decryptWith(is,os, key, type);
			is.close();
			os.close();
		}
	}
	
	
	public void PBEEncryptFiles() throws Exception{
		if(key==null)
			throw new Error("the key not set");
		if(this.isRandomSalt)
			PBESalt=DataEncryptor.initPBESalt();
		createSaltFile();
		Iterator<File> it=list.iterator();
		while(it.hasNext()){
			File tmp=it.next();
			showInfo(tmp.getAbsolutePath(),"Encrypt");
			File targetFile=new File(this.outputDir+File.separator+tmp.getName());
			if(targetFile.equals(tmp))
				throw new Error("the source file is equal to the target file");
			if(targetFile.exists()){
				Log.getLogger().debug("Warning: target file already exist, deleting");
				targetFile.delete();
			}
			targetFile.getParentFile().mkdirs();
			targetFile.createNewFile();
			
			InputStream is=new FileInputStream(tmp);
			OutputStream os=new FileOutputStream(targetFile);
			DataEncryptor.PBEEncrypt(is,os, key, DataEncryptor.PBEAlgorithmType.PBEWithMD5AndDES, this.PBESalt);
			is.close();
			os.close();
		}
	}
	
	public void PBEDecryptFiles() throws Exception{
		if(key==null&&!autoSetKey())
			throw new Error("the key not set");
		Iterator<File> it=list.iterator();
		while(it.hasNext()){
			File tmp=it.next();
			showInfo(tmp.getAbsolutePath(),"Decrypt");
			File targetFile=new File(this.outputDir+File.separator+tmp.getName());
			if(targetFile.equals(tmp))
				throw new Error("the source file is equal to the target file");
			if(targetFile.exists()){
					Log.getLogger().warn("Warning: target file already exist, deleting");
				targetFile.delete();
			}
			targetFile.getParentFile().mkdirs();
			targetFile.createNewFile();
			InputStream is=new FileInputStream(tmp);
			OutputStream os=new FileOutputStream(targetFile);
			DataEncryptor.PBEDecrypt(is,os, key, DataEncryptor.PBEAlgorithmType.PBEWithMD5AndDES, PBESalt);
			is.close();
			os.close();
		}
	}
	

	protected final void showInfo(String path,String method){
			Log.getLogger().debug(method+": "+path);
	}
	
	protected final String createSaltFile() throws IOException{
		UUID id=UUID.randomUUID();
			Log.getLogger().debug("Salt saved in file "+"\'SALT FILE "+id.toString()+"\'");
		File f=new File(this.outputDir+File.separator+"SALT FILE "+id.toString());
		buildFile(f);
		OutputStream os=new FileOutputStream(f);
		os.write(this.PBESalt);
		os.close();
		return "SALT FILE "+id.toString();
	}
	
	protected final String createKeyFile() throws IOException{
		UUID id=UUID.randomUUID();
		File f=new File(this.outputDir+File.separator+"KEY FILE "+id.toString());
		OutputStream os=new FileOutputStream(f);
		os.write(this.key.getBytes());
		os.close();
		return "KEY FILE "+id.toString();
	}
	
	private void buildFile(File f) throws IOException{
		if(!f.exists()||f.isDirectory()){
			f.getParentFile().mkdirs();
			f.createNewFile();
		}
	}
	
	private final boolean autoSetKey() throws IOException{
		Log.getLogger().debug("Key is not set, auto find start ...");
		int num=0;
		File[] keyFile=this.list.get(0).getParentFile().listFiles();
		String path=null;
		for(File f:keyFile){
			if(f.getName().startsWith("KEY FILE ")){
				num++;
				path=f.getAbsolutePath();
				if(num>=2)
					break;
			}
		}
		this.key="";
		if(num==1){
				Log.getLogger().debug("Use the key file "+path+" automaticly");
			BufferedReader br=FileUtil.getReader(path);
			int tmp=0;
			while((tmp=br.read())!=-1){
				this.key+=(char)tmp;
			}
			br.close();
			return true;
		}
			Log.getLogger().debug("Key file not found.");
		return false;
	}

}
